package cx3k.dataaccess;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import cx3k.dataaccess.exceptions.DataAccessException;
import cx3k.dataaccess.exceptions.NoManagerException;
import cx3k.scheduler.objects.Course;
import cx3k.scheduler.objects.Major;
import cx3k.scheduler.objects.Minor;
import cx3k.scheduler.objects.Profile;
import cx3k.scheduler.objects.Schedule;
import cx3k.scheduler.objects.Semester;

/**
 * @author Chris Thierer
 */
public class StandardDataAccessService implements DataAccessService {

	/**
	 * Static Standard Data Access Service, which is held in memory for the 
	 * duration of the program.
	 */
	private static StandardDataAccessService staticDAS = null;
	
	/**
	 * Log information to specified location.
	 */
	private static Logger logger = Logger.getLogger(StandardDataAccessService.class);
	
	/**
	 * Helper method to transform a List of Profile objects into a Map of
	 * Profile objects referenced by their Profile ID values.  
	 * @param 	profiles The List of profiles used to initialize the Map.  These
	 * 			objects are referenced in the Map, not duplicated.  
	 * @return	A Map of Profile IDs mapped to references of Profile taken 
	 * 			from the List parameter.
	 */
	private static Map<Integer, Profile> createProfileMapping(List<Profile> profiles){
		
		Map<Integer, Profile> profileMapping = new HashMap<Integer, Profile>();
		
		for(Profile profile : profiles){
			profileMapping.put(profile.getProfileId(), profile);
		}
		
		return profileMapping;
	}
	
	/**
	 * Helper method to transform a List of Schedule objects into a Map of
	 * Schedule objects referenced by their Schedule ID values.
	 * @param 	schedules The List of schedules used to initialize the Map.  
	 * 			These objects are referenced in the Map, not duplicated.
	 * @return	A Map of Schedule IDs mapped to references of Schedule taken
	 * 			from the List parameter.
	 */
	private static Map<Integer, Schedule> createScheduleMapping(List<Schedule> schedules){
		
		Map<Integer, Schedule> scheduleMapping = new HashMap<Integer, Schedule>();
		
		for(Schedule schedule : schedules){
			scheduleMapping.put(schedule.getScheduleId(), schedule);
		}
		
		return scheduleMapping;
	}
	
	/**
	 * Allow access to the Standard Data Access Service.  This treats
	 * the Standard Data Access Service as a singleton, in that it is only 
	 * available through this method, which guarantees that there is only
	 * one instance of the Service instantiated in memory at any given 
	 * moment.  Therefore, any objects referencing the Standard Data Access
	 * Service will rely on the same persistent data sources.  
	 * 
	 * @return	A current reference to the initialized Data Access Service.
	 */
	public static StandardDataAccessService getStandardDataAccessService(){
		
		if(staticDAS == null){
			staticDAS = new StandardDataAccessService(new ProfileManager(), null);
		}
		
		return staticDAS;
	}
	
	/**
	 * Manager providing access to information pertaining to a profile.
	 */
	private Manager<Profile> profileManager;
	
	/**
	 * Manager providing access to information pertaining to a schedule.
	 */
	private Manager<Schedule> scheduleManager;
	
	/**
	 * Mapping of courses loaded from persistent storage at object 
	 * initialization.  Maps Course IDs as key values to instances of Course
	 * loaded from storage.  This mapping will not reflect changes made
	 * to storage after its initial initialization.
	 */
	private Map<Integer, Course> courses;
	
	/**
	 * Mapping of majors loaded from persistent storage at object 
	 * initialization.  Maps Major IDs as key values to instances of Major
	 * loaded from storage.  This mapping will not reflect changes made 
	 * to storage after its initial initialization.
	 */
	private Map<Integer, Major> majors;
	
	/**
	 * Mapping of minors loaded from persistent storage at object 
	 * initialization.  Maps Minor IDs as key values to instances of Minor
	 * loaded from storage.  This mapping will not reflect changes made 
	 * to storage after its initial initialization.  
	 */
	private Map<Integer, Minor> minors;
	
	/**
	 * Mapping of semesters loaded from persistent storage at object
	 * initialization.  Maps Semester IDs as key values to instances of 
	 * Semester loaded from storage.  This mapping will not reflect changes
	 * made to storage after its initial initialization.  
	 */
	private Map<Integer, Semester> semesters;
	
	/**
	 * Create an instance of this Data Access Service, providing references to
	 * the initial Profile Manager and Schedule Manager.  
	 * @param 	profileManager Reference to the initial Profile Manager to be
	 * 			used by this Service.  The only way this will change is via
	 * 			the setProfileLocation() method, which is undefined for this
	 * 			implementation.
	 * @param 	scheduleManager Reference to the initial Schedule Manager to be
	 * 			used by this Service.  The only way this will change is via
	 * 			the setScheduleLocation() method.
	 */
	private StandardDataAccessService(Manager<Profile> profileManager, Manager<Schedule> scheduleManager){
		setProfileManager(profileManager);
		setScheduleManager(scheduleManager);
		
//		setCourses(CX3KDatabaseManager.getAllCourses());
		setMajors(CX3KDatabaseManager.getAllMajors());
		setMinors(CX3KDatabaseManager.getAllMinors());
		setSemesters(CX3KDatabaseManager.getAllSemesters());
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#deleteProfile(int)
	 */
	@Override
	public void deleteProfile(int profileId) throws DataAccessException {
		logger.trace("deleteProfile(" + profileId + ") invoked: Deleting profile where profile ID = " + profileId + ".");
		
		Profile deleted = getProfileManager().delete(profileId);
		
		if(deleted == null){
			logger.info("Could not delete object, profile ID not valid.");
		} else {
			logger.info("Deleted profile (Name: \"" + deleted.getName() + 
						"\", ID: "  + deleted.getProfileId() + ").");
		}
		
		logger.trace("Done: deleteProfile(" + profileId + ")");
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#deleteProfile(cx3k.scheduler.objects.Profile)
	 */
	@Override
	public void deleteProfile(Profile delete) throws DataAccessException {
		logger.trace("deleteProfile(" + delete + ") invoked: Deleting profile object were profile ID = " + delete.getProfileId() + ".");
		
		getProfileManager().delete(delete);
		
		logger.trace("Done: deleteProfile(" + delete + ")");
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#deleteSchedule(int)
	 */
	@Override
	public void deleteSchedule(int scheduleId) throws DataAccessException {
		logger.trace("deleteSchedule(" + scheduleId + ") invoked: Deleting schedule where schedule ID = " + scheduleId + ".");
		
		Schedule deleted = getScheduleManager().delete(scheduleId);
		
		if(deleted == null){
			logger.info("Could not delete object, schedule ID not valid.");
		} else {
			logger.info("Deleted schedule (ID: "  + deleted.getScheduleId() + ").");
		}
		
		logger.trace("Done: deleteSchedule(" + scheduleId + ")");
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#deleteSchedule(cx3k.scheduler.objects.Schedule)
	 */
	@Override
	public void deleteSchedule(Schedule schedule) throws DataAccessException {
		logger.trace("deleteScedule(" + schedule + ") invoked: Deleting schedule where schedule ID = " + schedule.getScheduleId() + ".");
		
		getScheduleManager().delete(schedule);
		
		logger.trace("Done: deleteSchedule(" + schedule + ")");
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getAllCourses()
	 */
	@Override
	public Map<Integer, Course> getAllCourses() {
		logger.trace("getAllCourses() invoked: Returning a Map of " + this.courses.size() + " elements.");
		return this.courses;
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getAllMajors()
	 */
	@Override
	public Map<Integer, Major> getAllMajors() {
		logger.trace("getAllMajors() invoked: Returning a Map of " + this.majors.size() + " elements.");
		return this.majors;
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getAllMinors()
	 */
	@Override
	public Map<Integer, Minor> getAllMinors() {
		logger.trace("getAllMinors() invoked: Returning a Map of " + this.minors.size() + " elements.");
		return this.minors;
	}
	
	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getAllSemesters()
	 */
	@Override
	public Map<Integer, Semester> getAllSemesters() {
		logger.trace("getAllSemesters() invoked: Returning a Map of " + this.semesters.size() + " elements.");
		return this.semesters;
	}
	
	/**
	 * Load a course, specified by the course ID passed in.  If the course ID
	 * does not map to an actual course, then null is returned.  The Course
	 * object returned will be initialized to match the data contained in 
	 * persistent storage.
	 * 
	 * @param	courseId The unique ID specified by the course to be loaded.
	 * @return	A course object that is initialized to match the information 
	 * 			contained within the storage; null if the ID value did notmap 
	 * 			to a record.
	 * @throws	DataAccessException Thrown if the persistent storage cannot be
	 * 			accessed.  
	 */
	public Course getCourse(int courseId) throws DataAccessException {
		return CX3KDatabaseManager.getCourse(courseId);
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getProfile(int)
	 */
	@Override
	public Profile getProfile(int profileId) throws DataAccessException {
		logger.trace("getProfile(" + profileId + ") invoked: Loading profile where profile ID = " + profileId + ".");
		
		Profile profile = getProfileManager().load(profileId);
		
		if(profile == null){
			logger.info("Could not load profile with ID = " + profileId + 
						"; ID does not match any elements.");
		} else {
			logger.info("Loaded profile (Name: \"" + profile.getName() +
						"\", ID: " + profile.getProfileId() + ").");
		}
		
		logger.trace("Done: getProfile(" + profileId + ")");
		
		return profile;
	}

	/**
	 * @return	The initialized Profile Manager for this Service.
	 * @throws	NoManagerException Thrown if the Profile Manager has not 
	 * 			been specified (it is null, or class invariants not met).  
	 */
	private Manager<Profile> getProfileManager() throws NoManagerException {
		
		if(profileManager == null){
			throw new NoManagerException("EXCEPTION: Profile Manager not specified.");
		}
		
		return profileManager;
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getProfiles()
	 */
	@Override
	public Map<Integer, Profile> getProfiles() throws DataAccessException {
		logger.trace("getProfiles() invoked: Loading all profiles from persistent storage...");
		
		return createProfileMapping(getProfileManager().loadAll());
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getSchedule(int)
	 */
	@Override
	public Schedule getSchedule(int scheduleId) throws DataAccessException {
		logger.trace("getSchedule(" + scheduleId + ") invoked: Loading profile where profile ID = " + scheduleId + ".");
		
		Schedule schedule = getScheduleManager().load(scheduleId);
		
		if(schedule == null){
			logger.info("Could not load schedule with ID = " + scheduleId + 
						"; ID does not match any elements.");
		} else {
			logger.info("Loaded schedule (ID: "  + schedule.getScheduleId() + ").");
		}
		
		logger.trace("Done: getSchedule(" + scheduleId + ")");
		
		return schedule;
	}

	/**
	 * @return	The initialized Schedule Manager for this Service.
	 * @throws	NoManagerException Thrown if the Schedule Manager has not 
	 * 			been specified (it is null, or class invariants not met).  
	 */
	private Manager<Schedule> getScheduleManager() throws NoManagerException {
		
		if(scheduleManager == null){
			throw new NoManagerException("EXCEPTION: Schedule Manager is not itialized.");
		}
		
		return scheduleManager;
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#getSchedules()
	 */
	@Override
	public Map<Integer, Schedule> getSchedules() throws DataAccessException {
		logger.trace("getSchedules() invoked: Loading all schedules from persistent storage...");
		
		return createScheduleMapping(getScheduleManager().loadAll());
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#saveProfile(cx3k.scheduler.objects.Profile)
	 */
	@Override
	public void saveProfile(Profile profile) throws DataAccessException {
		logger.trace("saveProfile(" + profile + ") invoked: Saving profile...");
		
		if(getProfileManager().save(profile)){
			logger.info("Profile (ID = " + profile.getProfileId() + " saved succesfully.");
		} else {
			logger.info("Profile (ID = " + profile.getProfileId() + " save failed.");
		}
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#saveProfiles(java.util.List)
	 */
	@Override
	public void saveProfiles(Collection<Profile> profiles) throws DataAccessException {
		logger.trace("saveProfiles(List, size = " + profiles.size() + ") invoked: Saving profiles...");
		
		for(Profile profile : profiles){
			saveProfile(profile);
		}
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#saveSchedule(cx3k.scheduler.objects.Schedule)
	 */
	@Override
	public void saveSchedule(Schedule schedule) throws DataAccessException {
		logger.trace("saveSchedule(" + schedule + ") invoked: Saving profile...");
		
		if(getScheduleManager().save(schedule)){
			logger.info("Schedule (ID = " + schedule.getScheduleId() + " saved succesfully.");
		} else {
			logger.info("Schedule (ID = " + schedule.getScheduleId() + " save failed.");
		}

	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#saveSchedules(java.util.List)
	 */
	@Override
	public void saveSchedules(Collection<Schedule> schedules) throws DataAccessException {
		logger.trace("saveSchedules(List, size = " + schedules.size() + ") invoked: Saving schedules...");
		
		for(Schedule schedule : schedules){
			saveSchedule(schedule);
		}
	}

	/**
	 * Set courses to reference the Map provided.  This will not make a 
	 * copy of the Map.
	 * @param 	courses Reference to the new Map of course IDs and courses.
	 */
	private void setCourses(Map<Integer, Course> courses){
		this.courses = courses;
	}

	/**
	 * Set majors to reference the Map provided.  This will not make a 
	 * copy of the Map.
	 * @param 	majors Reference to the new Map of major IDs and majors.
	 */
	private void setMajors(Map<Integer, Major> majors){
		this.majors = majors;
	}

	/**
	 * Set minors to reference the Map provided.  This will not make a 
	 * copy of the Map.
	 * @param 	minors Reference to the new Map of minor IDs and minors.
	 */
	private void setMinors(Map<Integer, Minor> minors){
		this.minors = minors;
	}

	/**
	 * This method is not defined for this definition of Data Access Service.
	 * @see cx3k.dataaccess.DataAccessService#setProfileLocation(java.lang.String)
	 */
	@Override
	public void setProfileLocation(String location) {
		/*
		 * This method is not defined in this definition of Data Access
		 * Service, as it is defined as optional in the interface.  This
		 * Service is designed to load information directly from a database,
		 * which is set in the Hibernate configuration file.  
		 */
	}

	/**
	 * Set the Profile Manager referenced in this Service to match the 
	 * parameter value specified.  This will not make a copy of the parameter
	 * specified.
	 * @param 	profileManager Reference to the new instance of Profile Manager,
	 * 			used to load profiles from persistent storage.
	 */
	private void setProfileManager(Manager<Profile> profileManager){
		this.profileManager = profileManager;
	}

	/* (non-Javadoc)
	 * @see cx3k.dataaccess.DataAccessService#setScheduleLocation(java.lang.String)
	 */
	@Override
	public void setScheduleLocation(String location) {
		setScheduleManager(new ScheduleManager(location));
	}

	/**
	 * Set the Schedule Manager referenced in this Service to match the
	 * parameter value specified.  This will not make a copy of the parameter
	 * specified.
	 * @param 	scheduleManager Reference to the new instance of Schedule 
	 * 			Manager used to load schedules from persistent storage.  
	 */
	private void setScheduleManager(Manager<Schedule> scheduleManager){
			this.scheduleManager = scheduleManager;
	}

	/**
	 * Set semesters to reference the Map provided.  This will not make a 
	 * copy of the Map.
	 * @param 	semesters Reference to the new Map of semester IDs and 
	 * 			semesters.
	 */
	private void setSemesters(Map<Integer, Semester> semesters){
		this.semesters = semesters;
	}
}
